Phạm Thành Long - 22022604¶
Github¶
Link source code tại Repository
1. Thêm các thư viện cần thiết¶
Ở đây, chúng ta sẽ sử dụng opencv2 cho việc xử lý hình ảnh, matplotlib để show ảnh, và một số thư viện phụ trợ khác.
import cv2
import matplotlib.pyplot as plt
import numpy as np
import os
import glob
import math
from tqdm import tqdm
2. Tải hình ảnh để sử dụng¶
Đọc ảnh cần tìm vật và chọn thử 1 đồ vật để hiển thị xem chúng như thế nào.
def show_image(image, figsize=None):
if figsize is not None:
plt.figure(figsize=figsize)
if len(image.shape) > 2:
image = image[...,::-1]
plt.imshow(image)
plt.axis('off')
def show(to_show, figsize=(10, 10), plot_size=None):
"""
Hiển thị một hoặc nhiều ảnh bằng Matplotlib.
Nếu chỉ có một ảnh, hàm sẽ hiển thị ảnh duy nhất đó.
Nếu có nhiều ảnh, hàm sẽ hiển thị theo lưới (grid) con dựa trên `plot_size` (nếu được cung cấp).
Args:
images (list of np.ndarray): Danh sách ảnh để hiển thị.
figsize (tuple of int, optional): Kích thước của figure. Mặc định là (10, 10).
plot_size (tuple of int, optional): Kích thước lưới hiển thị, dạng (rows, cols).
Nếu không được truyền vào, ảnh sẽ được hiển thị tuần tự trên các subplot liên tiếp.
Returns:
None: Hàm không trả về giá trị, chỉ hiển thị ảnh bằng matplotlib.
"""
# If you want to show only one images[0].
if not isinstance(to_show, list):
plt.figure(figsize=figsize)
show_image(to_show)
return
# Else
n = len(to_show)
if plot_size is None:
cols = int(math.ceil(math.sqrt(n)))
rows = int(math.ceil(n / cols))
else:
rows, cols = plot_size
# Hiển thị tuần tự
plt.figure(figsize=figsize)
for i, image in enumerate(to_show):
plt.subplot(rows, cols, i + 1)
show_image(image)
# Ẩn các subplot dư (nếu hàng*cột > n)
for idx in range(n, rows * cols):
plt.subplot(rows, cols, idx + 1)
plt.axis('off')
plt.tight_layout()
plt.show()
image = cv2.imread(f'Finding/2/image.png') # Ảnh cần tìm vật
template = cv2.imread(f'Finding/2/t1.png') # Template được chọn
show(image, figsize=(8, 5))
show(template, figsize=(1, 1))
Bây giờ hãy thử lấy ra toàn bộ template cần tìm và hiển thị chúng.
def get_all_templates(dir, name_tag='t'):
"""
Đọc tất cả file ảnh trong thư mục `dir` có tên bắt đầu với `name_tag`
và phần mở rộng .png. Trả về danh sách các ảnh (templates).
Args:
dir (str): Đường dẫn đến thư mục chứa ảnh.
name_tag (str, optional): Tiền tố dùng để lọc tên file. Mặc định là 't'.
Returns:
list[np.ndarray]: Danh sách các ảnh (templates) được đọc (dưới dạng numpy array).
"""
pattern = os.path.join(dir, f"{name_tag}*.png")
file_list = sorted(glob.glob(pattern))
templates = []
for path in file_list:
template = cv2.imread(path)
templates.append(template)
return templates
images = [cv2.imread(f'Finding/{i + 1}/image.png') for i in range(2)]
templates = [get_all_templates(f'Finding/{i + 1}') for i in range(2)]
print('Finding set 1:')
show(images[0], figsize=(5, 8))
show(templates[0], figsize=(3, 3))
print('Finding set 2:')
show(images[1], figsize=(5, 8))
show(templates[1], figsize=(3, 3))
Finding set 1:
Finding set 2:
Ở trên là ảnh ở dạng RGB. Tuy nhiên ta nên chuyển ảnh sang dạng ảnh mức xám, vì:
- Tăng tốc độ xử lý: Ảnh màu có 3 kênh (RGB), trong khi ảnh xám chỉ có 1 kênh, giúp giảm tài nguyên tính toán.
- Ổn định hơn với thay đổi màu sắc: Grayscale giúp tập trung vào hình dạng thay vì màu sắc, tránh nhiễu do thay đổi ánh sáng.
- Độ tương phản quan trọng hơn màu sắc: Các phương pháp so khớp dựa vào cường độ pixel thay vì màu sắc, nên ảnh xám cho kết quả chính xác hơn.
Ta chỉ giữ ảnh màu khi cần so khớp theo từng kênh hoặc màu sắc là yếu tố quan trọng.
def convert_color(images, color='rgb'):
"""
Chuyển đổi không gian màu cho một ảnh hoặc danh sách ảnh sang không gian màu chỉ định.
Args:
images (numpy.ndarray | list): Ảnh đầu vào hoặc danh sách ảnh.
color (str, optional): Kiểu màu muốn chuyển, như 'rgb' hoặc 'gray'. Mặc định là 'rgb'.
Returns:
numpy.ndarray | list: Ảnh (hoặc danh sách ảnh) sau khi chuyển đổi màu.
"""
if not isinstance(images, list):
return cv2.cvtColor(images, color)
res = []
for image in images:
res.append(cv2.cvtColor(image, color))
return res
# Chuyển sang ảnh mức xám
gray_images = convert_color(images, cv2.COLOR_BGR2GRAY)
gray_templates = [convert_color(template_set, cv2.COLOR_BGR2GRAY) for template_set in templates]
print('Finding set 1:')
show(gray_images[0], figsize=(5, 8))
show(gray_templates[0], figsize=(3, 3))
print('Finding set 2:')
show(gray_images[1], figsize=(5, 8))
show(gray_templates[1], figsize=(3, 3))
Finding set 1:
Finding set 2:
3. Finding¶
Yêu cầu của bài toán là tìm vật thể trong ảnh, vì vậy chúng ta sẽ hướng đến các phương pháp matching để tìm ra vị trí của vật thể.
Mô hình hóa: $$F(I, t) = (x, y, x', y')$$ Trong đó:
- I: hình ảnh chứa vật cần tìm
- t: vật cần tìm
- x, y: tọa độ góc trên bên trái, xét trong I
- x', y': tọa độ góc dưới bên phải, xét trong I
3.1 Thử cách đơn giản¶
Đầu tiên, chúng ta sẽ thử dùng template matching mà không điều chỉnh hay xử lý gì thêm.
def match_template(image, template, meth, mask=None):
"""
Thực hiện template matching với một phương pháp duy nhất trên một cặp ảnh/mặt nạ.
Trả về dict chứa vị trí top_left/bottom_right và các giá trị min_val, max_val.
"""
# Lấy constant của OpenCV từ meth (VD: CV_TM_CCOEFF, ...)
method = getattr(cv2, meth)
# Truyền thêm mask nếu cần
if mask is not None and method in [cv2.TM_SQDIFF, cv2.TM_CCORR]:
res = cv2.matchTemplate(image, template, method, mask=mask)
else:
res = cv2.matchTemplate(image, template, method)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
# Tùy vào phương pháp, top_left sẽ là min_loc hoặc max_loc
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
top_left = min_loc
best_value = min_val
else:
top_left = max_loc
best_value = max_val
h, w = template.shape[:2]
bottom_right = (top_left[0] + w, top_left[1] + h)
return {
"top_left": top_left,
"bottom_right": bottom_right,
"min_val": min_val,
"max_val": max_val,
"best_value": best_value,
"method": meth
}
def draw_bounding_box(image, match_result, color=(0, 0, 0), in_place=False, thickness=4):
"""
Vẽ bounding box trực tiếp lên ảnh dựa trên kết quả match_result
rồi trả về ảnh kết quả. Không sử dụng matplotlib để hiển thị.
Args:
image (np.ndarray): Ảnh gốc (BGR hoặc grayscale).
match_result (dict | list[dict]):
- dict: dạng {
"top_left": (x, y),
"bottom_right": (x, y),
"min_val": float,
"max_val": float,
"best_value": float,
"method": str (tuỳ chọn)
}
- list[dict]: danh sách các dict giống trên.
Returns:
np.ndarray: Ảnh đã được vẽ bounding box (cùng kích thước với ảnh gốc).
"""
import cv2
# Nếu match_result là dict, gói thành danh sách để nạp vòng lặp
if isinstance(match_result, dict):
match_result = [match_result]
if in_place:
image_to_draw = image
else:
image_to_draw = image.copy()
# Vẽ bounding box cho từng dict
for res in match_result:
top_left = res["top_left"]
bottom_right = res["bottom_right"]
cv2.rectangle(image_to_draw, top_left, bottom_right, color, thickness)
return image_to_draw
Thử thực hiện matching đơn giản với SQDIFF (sum of squared difference), CCOEFF (correlation coefficient) và CCORR (cross-correlation) để đánh giá kết quả.
methods = [
"TM_CCOEFF",
"TM_CCOEFF_NORMED",
"TM_CCORR",
"TM_CCORR_NORMED",
"TM_SQDIFF",
"TM_SQDIFF_NORMED"
]
# Một số màu ví dụ
colors = [
(255, 0, 0), # Đỏ
(0, 255, 0), # Xanh lá
(0, 0, 255), # Xanh dương
(255, 255, 0), # Vàng
(255, 0, 255), # Hồng
(0, 255, 255) # Xanh biển
]
results = []
for i, image in enumerate(images):
img_res = image.copy()
for j, method in enumerate(methods):
match_result = match_template(gray_images[i], gray_templates[i][0], method)
color = colors[j][::-1]
img_res = draw_bounding_box(img_res, match_result, color, in_place=True, thickness=4)
results.append(img_res)
for i in range(2):
show(templates[i][0], figsize=(2, 2))
show(results[i], figsize=(6, 10))
Có thể thấy rằng kết quả không tốt chút nào, toàn bộ các phương pháp đều tìm sai. Vậy cần phải xác định được nguyên nhân.
| Nguyên nhân | Giải pháp đề xuất | |
|---|---|---|
| 1 | Sự khác biệt giữa kích thước của vật cần tìm với kích thước của vật đó trong ảnh, do đề bài và quá trình cắt ảnh | Sử dụng multiscale template matching |
| 2 | Sự khác biệt về vùng xung quanh vật trong template và trong image | Sử dụng masking để loại bỏ vùng không cần thiết |
3.2 Cải tiến cách vừa rồi¶
Từ những vấn đề vừa xác định ở trên, tiếp theo chúng ta sẽ thực hiện masking (xóa bỏ background của template) và sau đó thực hiện template matching trên nhiều scale khác nhau của template đó.
def create_expanded_mask(img, threshold_value=240):
"""
Tạo mask nhị phân bằng ngưỡng, và mở rộng nó thêm 2 đơn vị
(để phù hợp floodFill).
"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) > 2 else img
_, mask_bin = cv2.threshold(gray, threshold_value, 255, cv2.THRESH_BINARY)
mask_bin = cv2.bitwise_not(mask_bin)
h, w = gray.shape
mask_expanded = np.zeros((h + 2, w + 2), np.uint8)
mask_expanded[1:-1, 1:-1] = mask_bin
return mask_expanded
def flood_fill_edges(channel, mask_expanded, lo_diff=20, up_diff=20):
"""
Sử dụng floodFill cho từng kênh, duyệt qua tất cả điểm trên 4 cạnh
để lấp toàn bộ nền liên thông.
"""
h, w = channel.shape[:2]
temp_mask = mask_expanded.copy()
# Top row & bottom row
for x in range(w):
cv2.floodFill(channel, temp_mask, (x, 0), 0, loDiff=(lo_diff,), upDiff=(up_diff,))
cv2.floodFill(channel, temp_mask, (x, h - 1), 0, loDiff=(lo_diff,), upDiff=(up_diff,))
# Left column & right column
for y in range(h):
cv2.floodFill(channel, temp_mask, (0, y), 0, loDiff=(lo_diff,), upDiff=(up_diff,))
cv2.floodFill(channel, temp_mask, (w - 1, y), 0, loDiff=(lo_diff,), upDiff=(up_diff,))
return channel
def remove_bg(templates, lo_diff=20, up_diff=20, threshold_value=240):
"""
Loại bỏ background của danh sách vật thể (templates) bằng kỹ thuật floodFill
với seed points trên 4 cạnh của ảnh, sau đó thực hiện morphological opening.
Args:
templates (list of np.ndarray]): Danh sách ảnh BGR cần xử lý.
lo_diff (int): Ngưỡng sai khác màu dưới.
up_diff (int): Ngưỡng sai khác màu trên.
Returns:
list of np.ndarray: Ảnh đã xóa nền (vùng nền được tô đen).
"""
if not isinstance(templates, list):
templates = [templates]
results = []
for template in templates:
if template is None:
results.append(None)
continue
# Tạo mask expanded
mask_expanded_original = create_expanded_mask(template, threshold_value)
if len(template.shape) > 2:
# Tách kênh B, G, R
b, g, r = cv2.split(template)
# Flood fill trên từng kênh
b = flood_fill_edges(b, mask_expanded_original, lo_diff, up_diff)
g = flood_fill_edges(g, mask_expanded_original, lo_diff, up_diff)
r = flood_fill_edges(r, mask_expanded_original, lo_diff, up_diff)
# Gộp lại
filled = cv2.merge((b, g, r))
else:
filled = flood_fill_edges(template, mask_expanded_original, lo_diff, up_diff)
# Morphological opening (loại bỏ nhiễu)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
filled = cv2.morphologyEx(filled, cv2.MORPH_OPEN, kernel)
results.append(filled)
return results
def remove_bg_grabcut(templates, bgd_model_iters=5):
"""
Loại bỏ phông nền bằng GrabCut cho danh sách ảnh (templates).
GrabCut thường cho kết quả chính xác hơn floodFill trong nhiều trường hợp.
Args:
templates (list of np.ndarray): Danh sách ảnh BGR cần tách nền.
bgd_model_iters (int): Số lần lặp cho mô hình nền (thường 5 đến 10).
Returns:
list of np.ndarray: Danh sách ảnh đã được tách nền (nền chuyển thành đen).
"""
if not isinstance(templates, list):
templates = [templates]
results = []
for img in templates:
if img is None:
results.append(None)
continue
# Lấy kích thước ảnh
h, w = img.shape[:2]
# Tạo mask rỗng
grabcut_mask = np.zeros((h, w), np.uint8)
# Chuẩn bị 2 mô hình GMM cho GrabCut
bgd_model = np.zeros((1, 65), np.float64)
fgd_model = np.zeros((1, 65), np.float64)
# Tạo vùng bao (rectangle) gần như khớp toàn bộ ảnh
rect = (1, 1, w-2, h-2)
# Áp dụng GrabCut
# mode=GC_INIT_WITH_RECT: khỏi tạo bằng rectangle
cv2.grabCut(img, grabcut_mask, rect, bgd_model, fgd_model, bgd_model_iters, cv2.GC_INIT_WITH_RECT)
# Tạo mask foreground = 1 hoặc 3 => vùng nền = 0,2 => gán 0
mask_fg = np.where((grabcut_mask==2)|(grabcut_mask==0), 0, 1).astype('uint8')
# Nhân mask_fg vào ảnh gốc => giữ foreground, nền thành đen
img_nobg = cv2.bitwise_and(img, img, mask=mask_fg)
results.append(img_nobg)
return results
Check xem trong 2 phương pháp này thì phương pháp nào tốt hơn.
b0 = convert_color(templates[0][1], cv2.COLOR_BGR2GRAY)
b1 = convert_color(remove_bg(templates[0][1]), cv2.COLOR_BGR2GRAY)[0]
b2 = convert_color(remove_bg_grabcut(templates[0][1]), cv2.COLOR_BGR2GRAY)[0]
show([b0, b1, b2], plot_size=(1, 3), figsize=(3, 1))
show(b1 == b0, figsize=(2, 2))
show(b2 == b0, figsize=(2, 2))
Ok, vậy là cách dùng floodFill có vẻ tốt hơn, hãy sử dụng cách này vào bài toán.
rmgb_templates = [remove_bg(template_set) for template_set in templates]
for template_set in rmgb_templates:
show(template_set, figsize=(3, 3))
rmgb_templates[0][0][..., 0]
array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], dtype=uint8)
Như vậy là ta đã xóa được background của template, phương pháp trên đạt độ chính xác rất cao, đã đáp ứng mong muốn hiện thời.
Tiếp theo, chúng ta sẽ tiến hành sử dụng template matching với nhiều scale khác nhau cho vật cần tìm. Sử dụng masking với từ ảnh đã loại bỏ background để bỏ qua phần nhiễu ảnh hưởng đến kết quả
def prepare_template_and_mask(template, img_color_space):
"""
Nếu ảnh gốc là grayscale thì chuyển template sang gray.
Ngược lại vẫn dùng template gốc, đồng thời tạo mask bằng ngưỡng trên ảnh gray.
"""
# Nếu ảnh gốc là gray
if img_color_space == 'gray':
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) if len(template.shape) > 2 else template
template_used = template_gray
else:
# Ảnh gốc là color
template_used = template
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) if len(template.shape) > 2 else template
# Tạo mask foreground bằng threshold
_, mask = cv2.threshold(template_gray, 5, 255, cv2.THRESH_BINARY)
return template_used, mask
def search_best_scale(img, template_used, meth, scales, mask=None, show_progress=True):
"""
Duyệt qua các 'scales' để resize template và mask,
gọi lại hàm match_template để lấy kết quả,
trả về (best_loc, best_scale, best_min_val, best_max_val).
"""
h, w = template_used.shape[:2]
method = getattr(cv2, meth)
# Xác định giá trị khởi tạo tùy vào phương pháp
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
best_val = float('inf')
else:
best_val = float('-inf')
best_loc, best_scale = None, None
best_min_val, best_max_val = None, None
iterator = tqdm(scales, unit='scale', desc='\t') if show_progress else scales
# Duyệt qua từng scale để chọn kết quả hợp lý nhất
for scale in iterator:
new_h, new_w = int(h * scale), int(w * scale)
if new_h < 1 or new_w < 1:
continue # Bỏ qua nếu scale làm template về kích thước <= 0
template_resized = cv2.resize(template_used, (new_w, new_h))
if mask is not None:
mask_resized = cv2.resize(mask, (new_w, new_h))
else:
mask_resized = None
# Gọi hàm match_template đã viết
res_dict = match_template(img, template_resized, meth, mask=mask_resized)
min_val, max_val = res_dict["min_val"], res_dict["max_val"]
loc = res_dict["top_left"]
cur_best_value = res_dict["best_value"]
# Cập nhật kết quả tốt nhất
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
if cur_best_value < best_val:
best_val = cur_best_value
best_loc = loc
best_scale = scale
best_min_val = min_val
best_max_val = max_val
else:
if cur_best_value > best_val:
best_val = cur_best_value
best_loc = loc
best_scale = scale
best_min_val = min_val
best_max_val = max_val
return best_loc, best_scale, best_min_val, best_max_val
def compute_bounding_box(best_loc, best_scale, w, h):
"""
Tính toạ độ góc trên - trái và dưới - phải cho bounding box dựa trên best_scale.
"""
top_left = best_loc
h_resize = int(h * best_scale)
w_resize = int(w * best_scale)
bottom_right = (top_left[0] + w_resize, top_left[1] + h_resize)
return top_left, bottom_right
def template_matching_best_scale(templates, img, meth, scales, use_mask=True, show_progress=True):
"""
Thực hiện Template Matching với nhiều scale khác nhau trên danh sách template.
Args:
templates (list of np.ndarray): Danh sách các template cần tìm trên ảnh.
img (np.ndarray): Ảnh gốc để thực hiện template matching.
meth (str): Tên phương pháp template matching, ví dụ "TM_CCOEFF_NORMED".
scales (list of float): Danh sách tỉ lệ scale để resize (vd: [0.5, 1.0, 1.5]).
Returns:
list of dict: Mỗi phần tử chứa thông tin bounding box:
{
"top_left": (x, y),
"bottom_right": (x, y),
"min_val": float,
"max_val": float,
"best_value": float,
"method": str
}
"""
# Xác định kiểu ảnh gốc (color hoặc gray)
img_color_space = 'rgb' if len(img.shape) > 2 else 'gray'
results = []
for i, template in enumerate(templates):
if show_progress:
print(f'Template {i + 1}/{len(templates)}')
# Chuẩn bị template và mask
template_used, mask = prepare_template_and_mask(template, img_color_space)
if not use_mask:
mask = None
# Tìm best_loc, best_scale
best_loc, best_scale, best_min_val, best_max_val = search_best_scale(
img, template_used, meth, scales, mask, show_progress
)
# Tính bounding box
h, w = template_used.shape[:2]
top_left, bottom_right = compute_bounding_box(best_loc, best_scale, w, h)
# Xác định best_value
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
best_value = best_min_val
else:
best_value = best_max_val
# Lưu dict kết quả
results.append({
"top_left": top_left,
"bottom_right": bottom_right,
"min_val": best_min_val,
"max_val": best_max_val,
"best_value": best_value,
"method": meth
})
return results
scales = np.arange(1.0, 0.6, -0.02)
print(len(scales))
methods = [
"TM_SQDIFF"
]
for image, gray_image, template_set in zip(images, gray_images, rmgb_templates):
img_res = image.copy()
for i, method in enumerate(methods):
match_result = template_matching_best_scale(templates=template_set, meth=method, img=gray_image, scales=scales, show_progress=False)
img_res = draw_bounding_box(img_res, match_result)
show(template_set, figsize=(2, 2))
show(img_res, figsize=(5, 9))
20
scales = np.arange(1.2, 0.55, -0.02)
print(len(scales))
methods = [
"TM_SQDIFF"
]
for image, gray_image, template_set in zip(images, gray_images, rmgb_templates):
img_res = image.copy()
for i, method in enumerate(methods):
match_result = template_matching_best_scale(templates=template_set, meth=method, img=gray_image, scales=scales, show_progress=False)
img_res = draw_bounding_box(img_res, match_result)
show(template_set, figsize=(2, 2))
show(img_res, figsize=(5, 9))
33
def debug(templates, img, meth, scales):
# Xác định kiểu ảnh gốc (color hoặc gray)
img_color_space = 'rgb' if len(img.shape) > 2 else 'gray'
results = []
for i, template in enumerate(templates):
# Chuẩn bị template và mask
template_used, mask = prepare_template_and_mask(template, img_color_space)
# Tìm best_loc, best_scale
h, w = template_used.shape[:2]
method = getattr(cv2, meth)
# Khởi tạo giá trị best
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
best_val = float('inf')
else:
best_val = float('-inf')
# Danh sách lưu thông tin debug
debug_info = []
for scale in scales:
new_h, new_w = int(h * scale), int(w * scale)
if new_h < 1 or new_w < 1:
continue
# Resize template & mask
template_resized = cv2.resize(template_used, (new_w, new_h))
mask_resized = cv2.resize(mask, (new_w, new_h))
# Gọi match_template
res_dict = match_template(img, template_resized, meth, mask=mask_resized)
loc = res_dict["top_left"]
cur_best_value = res_dict["best_value"]
changed = False
# Cập nhật "best"
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
if cur_best_value < best_val:
changed = True
best_val = cur_best_value
else:
if cur_best_value > best_val:
changed = True
best_val = cur_best_value
# Lấy vùng match (ROI) quanh loc trên ảnh gốc
roi = None
y, x = loc[1], loc[0] # loc = (x, y)
if (y >= 0 and y + new_h <= img.shape[0]) and (x >= 0 and x + new_w <= img.shape[1]):
roi = img[y:y+new_h, x:x+new_w]
# Lưu debug
debug_info.append({
"scale": scale,
"template_resized": template_resized,
"roi": roi,
"value": cur_best_value,
"changed": changed
})
# Lưu dict kết quả
results.append(debug_info)
return results
Scales chọn là scales = np.arange(1.3, 0.55, -0.02)
Template được chọn là con thuyền (7) của ảnh 1, và con bướm (6) của ảnh 2. Đồng thời so sánh overlap rate giữa các kết quả của từng scale.
def overlap_rate(image1, image2):
overlap_mask = image1 == image2
show(overlap_mask, figsize=(2, 2))
overlap_sum = np.sum(overlap_mask)
return np.log(overlap_sum)
scales = np.arange(1.2, 0.55, -0.02)
print(len(scales))
method = "TM_SQDIFF"
boat_debug = debug(templates=[rmgb_templates[0][7]], meth=method, img=gray_images[0], scales=scales) # [num_temps, num_scales, {}]
butt_debug = debug(templates=[rmgb_templates[1][6]], meth=method, img=gray_images[1], scales=scales) # [num_temps, num_scales, {}]
print('================================================')
best_rate = 0
for res in boat_debug[0]:
rate = overlap_rate(res['template_resized'], res['roi'])
gr = False
if rate > best_rate:
best_rate = rate
gr = True
print(f'Scale = {res['scale']:.2f} || Value = {res['value']:.2f} || Changed = {res['changed']} || OVR = {rate:.2f} || Greater = {gr}')
to_show = [
res['template_resized'],
res['roi']
]
show(to_show, figsize=(2, 2))
print('================================================')
best_rate = 0
for res in butt_debug[0]:
rate = overlap_rate(res['template_resized'], res['roi'])
gr = False
if rate > best_rate:
best_rate = rate
gr = True
print(f'Scale = {res['scale']:.2f} || Value = {res['value']:.2f} || Changed = {res['changed']} || OVR = {rate:.2f} || Greater = {gr}')
to_show = [
res['template_resized'],
res['roi']
]
show(to_show, figsize=(2, 2))
33 ================================================ Scale = 1.20 || Value = 23972948.00 || Changed = True || OVR = 4.32 || Greater = True
Scale = 1.18 || Value = 24102038.00 || Changed = False || OVR = 4.22 || Greater = False
Scale = 1.16 || Value = 21902230.00 || Changed = True || OVR = 4.23 || Greater = False
Scale = 1.14 || Value = 20975200.00 || Changed = True || OVR = 3.74 || Greater = False
Scale = 1.12 || Value = 19460152.00 || Changed = True || OVR = 3.58 || Greater = False
Scale = 1.10 || Value = 19217070.00 || Changed = True || OVR = 3.97 || Greater = False
Scale = 1.08 || Value = 17426774.00 || Changed = True || OVR = 3.89 || Greater = False
Scale = 1.06 || Value = 16566539.00 || Changed = True || OVR = 4.03 || Greater = False
Scale = 1.04 || Value = 15289204.00 || Changed = True || OVR = 4.04 || Greater = False
Scale = 1.02 || Value = 14595577.00 || Changed = True || OVR = 4.06 || Greater = False
Scale = 1.00 || Value = 15610425.00 || Changed = False || OVR = 3.37 || Greater = False
Scale = 0.98 || Value = 14683706.00 || Changed = False || OVR = 3.58 || Greater = False
Scale = 0.96 || Value = 14018260.00 || Changed = True || OVR = 3.47 || Greater = False
Scale = 0.94 || Value = 12536886.00 || Changed = True || OVR = 3.00 || Greater = False
Scale = 0.92 || Value = 11761000.00 || Changed = True || OVR = 3.37 || Greater = False
Scale = 0.90 || Value = 11075222.00 || Changed = True || OVR = 3.04 || Greater = False
Scale = 0.88 || Value = 10797841.00 || Changed = True || OVR = 3.22 || Greater = False
Scale = 0.86 || Value = 9902244.00 || Changed = True || OVR = 3.09 || Greater = False
Scale = 0.84 || Value = 9035764.00 || Changed = True || OVR = 2.77 || Greater = False
Scale = 0.82 || Value = 8368385.00 || Changed = True || OVR = 3.04 || Greater = False
Scale = 0.80 || Value = 8452138.00 || Changed = False || OVR = 2.56 || Greater = False
Scale = 0.78 || Value = 7348840.00 || Changed = True || OVR = 2.08 || Greater = False
Scale = 0.76 || Value = 7434645.00 || Changed = False || OVR = 2.64 || Greater = False
Scale = 0.74 || Value = 6313179.00 || Changed = True || OVR = 2.71 || Greater = False
Scale = 0.72 || Value = 6391211.00 || Changed = False || OVR = 2.48 || Greater = False
Scale = 0.70 || Value = 6802458.00 || Changed = False || OVR = 2.71 || Greater = False
Scale = 0.68 || Value = 6601617.00 || Changed = False || OVR = 2.56 || Greater = False
Scale = 0.66 || Value = 5839766.00 || Changed = True || OVR = 2.48 || Greater = False
Scale = 0.64 || Value = 4459746.00 || Changed = True || OVR = 5.86 || Greater = True
Scale = 0.62 || Value = 4646339.00 || Changed = False || OVR = 5.80 || Greater = False
Scale = 0.60 || Value = 4223606.00 || Changed = True || OVR = 5.71 || Greater = False
Scale = 0.58 || Value = 4012457.00 || Changed = True || OVR = 5.65 || Greater = False
Scale = 0.56 || Value = 3529773.00 || Changed = True || OVR = 2.48 || Greater = False
================================================ Scale = 1.20 || Value = 57898280.00 || Changed = True || OVR = 3.58 || Greater = True
Scale = 1.18 || Value = 55722128.00 || Changed = True || OVR = 3.66 || Greater = True
Scale = 1.16 || Value = 53266368.00 || Changed = True || OVR = 3.40 || Greater = False
Scale = 1.14 || Value = 53386772.00 || Changed = False || OVR = 3.14 || Greater = False
Scale = 1.12 || Value = 50219236.00 || Changed = True || OVR = 2.83 || Greater = False
Scale = 1.10 || Value = 47695500.00 || Changed = True || OVR = 2.40 || Greater = False
Scale = 1.08 || Value = 44527676.00 || Changed = True || OVR = 2.40 || Greater = False
Scale = 1.06 || Value = 42162988.00 || Changed = True || OVR = 2.77 || Greater = False
Scale = 1.04 || Value = 41164752.00 || Changed = True || OVR = 2.71 || Greater = False
Scale = 1.02 || Value = 36825932.00 || Changed = True || OVR = 2.20 || Greater = False
Scale = 1.00 || Value = 39360220.00 || Changed = False || OVR = 2.71 || Greater = False
Scale = 0.98 || Value = 36679528.00 || Changed = True || OVR = 2.30 || Greater = False
Scale = 0.96 || Value = 34173388.00 || Changed = True || OVR = 2.08 || Greater = False
Scale = 0.94 || Value = 31954672.00 || Changed = True || OVR = 2.71 || Greater = False
Scale = 0.92 || Value = 30911656.00 || Changed = True || OVR = 2.40 || Greater = False
Scale = 0.90 || Value = 27928336.00 || Changed = True || OVR = 2.20 || Greater = False
Scale = 0.88 || Value = 26490452.00 || Changed = True || OVR = 2.20 || Greater = False
Scale = 0.86 || Value = 25262164.00 || Changed = True || OVR = 2.94 || Greater = False
Scale = 0.84 || Value = 23228684.00 || Changed = True || OVR = 2.89 || Greater = False
Scale = 0.82 || Value = 21057182.00 || Changed = True || OVR = 2.56 || Greater = False
Scale = 0.80 || Value = 16673123.00 || Changed = True || OVR = 2.77 || Greater = False
Scale = 0.78 || Value = 11295360.00 || Changed = True || OVR = 1.95 || Greater = False
Scale = 0.76 || Value = 11159560.00 || Changed = True || OVR = 1.95 || Greater = False
Scale = 0.74 || Value = 13791212.00 || Changed = False || OVR = 1.39 || Greater = False
Scale = 0.72 || Value = 14302447.00 || Changed = False || OVR = 2.77 || Greater = False
Scale = 0.70 || Value = 13540681.00 || Changed = False || OVR = 1.61 || Greater = False
Scale = 0.68 || Value = 14120190.00 || Changed = False || OVR = 1.79 || Greater = False
Scale = 0.66 || Value = 12953801.00 || Changed = False || OVR = 1.79 || Greater = False
Scale = 0.64 || Value = 12789100.00 || Changed = False || OVR = 1.61 || Greater = False
Scale = 0.62 || Value = 12074352.00 || Changed = False || OVR = 1.61 || Greater = False
Scale = 0.60 || Value = 11677221.00 || Changed = False || OVR = 0.69 || Greater = False
Scale = 0.58 || Value = 10538159.00 || Changed = True || OVR = -inf || Greater = False
C:\Users\longt\AppData\Local\Temp\ipykernel_3652\4110614384.py:7: RuntimeWarning: divide by zero encountered in log return np.log(overlap_sum)
Scale = 0.56 || Value = 9263853.00 || Changed = True || OVR = 0.00 || Greater = False
- Xét đến con thuyền, có thể thấy rằng ở
scale = 0.64đã tìm được vị trí thích hợp, tuy nhiên, ởscale = 0.56lại tìm ra vị trí khác là sai nhưng lại có sốđiểm tốt hơn. - Xét đến con bướm,
scale = 0.80đã cho kết quả chính xác, nhưng lại bị thay thế bởiscale = 0.6với số điểm tốt hơn mặc dù là sai. - Đồng thời,
overlap ratevẫn chưa cho thấy được tính quyết định của mình.
Có thể nhận xét rằng scale nhỏ hơn khiến kích thước template nhỏ đi và match vào các vị trí không đúng nhưng lại có độ tương đồng cao hơn về màu sắc hoặc đặc điểm nào đó . Vì thế, cần phải đưa ra phương pháp xử lý vấn đề kiểm tra kết quả sau khi match để xem liệu nó có nên
được thay đổi hay không.
3.3 Sử dụng phương pháp phát hiện cạnh¶
def to_edges(img, blur_ksize=3, threshold1=50, threshold2=200):
"""
Tiền xử lý ảnh bằng Canny:
1) Chuyển sang gray nếu cần
2) Làm mờ ảnh (GaussianBlur)
3) Lấy biên cạnh (Canny)
4) Áp dụng phép đóng (morphological close) để nối các cạnh đứt đoạn
"""
# Nếu là ảnh BGR, chuyển về gray
if len(img.shape) == 3:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
gray = img
# Giảm nhiễu
# blurred = cv2.GaussianBlur(gray, (blur_ksize, blur_ksize), 0)
# Morphological close để lấp kín các lỗ hổng nhỏ
# kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# blurred = cv2.morphologyEx(blurred, cv2.MORPH_OPEN, kernel)
# Lấy biên cạnh
edges = cv2.Canny(gray, threshold1, threshold2)
return edges
edged_images = [to_edges(img) for img in images]
edged_templates = []
for tmpl_list in templates:
edgelist = [to_edges(tmpl) for tmpl in tmpl_list]
edged_templates.append(edgelist)
for image, tempset in zip(edged_images, edged_templates):
show(tempset, figsize=(3, 3))
show(image, figsize=(5, 8))
scales = np.arange(1.0, 0.51, -0.01)
print(len(scales))
methods = [
"TM_CCOEFF_NORMED"
]
for image, edged_image, template_set in zip(images, edged_images, edged_templates):
img_res = image.copy()
for i, method in enumerate(methods):
match_result = template_matching_best_scale(templates=template_set, meth=method, img=edged_image, scales=scales, show_progress=False, use_mask=False)
img_res = draw_bounding_box(img_res, match_result)
show(template_set, figsize=(3, 3))
show(img_res, figsize=(5, 9))
49
Quả bóng ở ảnh 1 vẫn chưa được tìm ra chính xác, hãy tiến hành debug xem có vấn đề xảy ra ở đâu.
def iou(image1, image2, show_=False):
object_mask = image1 != 0
object_in_background_mask = image2 != 0
intersection = object_mask & object_in_background_mask
if show_:
show(intersection, figsize=(1, 1))
overlap_sum = np.sum(intersection)
union = np.sum(object_mask | object_in_background_mask) # Cả hai ảnh có kích thước giống nhau
similarity_score = overlap_sum / union if union != 0 else 0
return similarity_score
scales = np.arange(1.0, 0.51, -0.01)
print(len(scales))
method = "TM_CCOEFF_NORMED"
ball_debug = debug(templates=[edged_templates[0][11]], meth=method, img=edged_images[0], scales=scales) # [num_temps, num_scales, {}]
print('================================================')
best_iou = 0
for res in ball_debug[0]:
simm = iou(res['template_resized'], res['roi'], show_=True)
gr = False
if simm > best_iou:
best_iou = simm
gr = True
print(f'Scale = {res['scale']:.2f} || Value = {res['value']:.2f} || Changed = {res['changed']} || IOU = {simm:.2f} || Greater = {gr}')
to_show = [
res['template_resized'],
res['roi']
]
show(to_show, figsize=(2, 2))
print('================================================')
49 ================================================ Scale = 1.00 || Value = 0.14 || Changed = True || IOU = 0.13 || Greater = True
Scale = 0.99 || Value = 0.16 || Changed = True || IOU = 0.17 || Greater = True
Scale = 0.98 || Value = 0.16 || Changed = False || IOU = 0.17 || Greater = True
Scale = 0.97 || Value = 0.17 || Changed = True || IOU = 0.18 || Greater = True
Scale = 0.96 || Value = 0.18 || Changed = True || IOU = 0.18 || Greater = False
Scale = 0.95 || Value = 0.16 || Changed = False || IOU = 0.17 || Greater = False
Scale = 0.94 || Value = 0.16 || Changed = False || IOU = 0.19 || Greater = True
Scale = 0.93 || Value = 0.16 || Changed = False || IOU = 0.19 || Greater = True
Scale = 0.92 || Value = 0.15 || Changed = False || IOU = 0.18 || Greater = False
Scale = 0.91 || Value = 0.15 || Changed = False || IOU = 0.15 || Greater = False
Scale = 0.90 || Value = 0.17 || Changed = False || IOU = 0.19 || Greater = False
Scale = 0.89 || Value = 0.17 || Changed = False || IOU = 0.16 || Greater = False
Scale = 0.88 || Value = 0.17 || Changed = False || IOU = 0.16 || Greater = False
Scale = 0.87 || Value = 0.19 || Changed = True || IOU = 0.16 || Greater = False
Scale = 0.86 || Value = 0.17 || Changed = False || IOU = 0.17 || Greater = False
Scale = 0.85 || Value = 0.19 || Changed = True || IOU = 0.17 || Greater = False
Scale = 0.84 || Value = 0.19 || Changed = False || IOU = 0.17 || Greater = False
Scale = 0.83 || Value = 0.18 || Changed = False || IOU = 0.17 || Greater = False
Scale = 0.82 || Value = 0.18 || Changed = False || IOU = 0.17 || Greater = False
Scale = 0.81 || Value = 0.17 || Changed = False || IOU = 0.18 || Greater = False
Scale = 0.80 || Value = 0.18 || Changed = False || IOU = 0.17 || Greater = False
Scale = 0.79 || Value = 0.18 || Changed = False || IOU = 0.17 || Greater = False
Scale = 0.78 || Value = 0.20 || Changed = True || IOU = 0.22 || Greater = True
Scale = 0.77 || Value = 0.21 || Changed = True || IOU = 0.21 || Greater = False
Scale = 0.76 || Value = 0.19 || Changed = False || IOU = 0.19 || Greater = False
Scale = 0.75 || Value = 0.20 || Changed = False || IOU = 0.19 || Greater = False
Scale = 0.74 || Value = 0.21 || Changed = True || IOU = 0.20 || Greater = False
Scale = 0.73 || Value = 0.24 || Changed = True || IOU = 0.24 || Greater = True
Scale = 0.72 || Value = 0.23 || Changed = False || IOU = 0.22 || Greater = False
Scale = 0.71 || Value = 0.22 || Changed = False || IOU = 0.22 || Greater = False
Scale = 0.70 || Value = 0.23 || Changed = False || IOU = 0.23 || Greater = False
Scale = 0.69 || Value = 0.21 || Changed = False || IOU = 0.23 || Greater = False
Scale = 0.68 || Value = 0.24 || Changed = True || IOU = 0.23 || Greater = False
Scale = 0.67 || Value = 0.24 || Changed = False || IOU = 0.23 || Greater = False
Scale = 0.66 || Value = 0.23 || Changed = False || IOU = 0.24 || Greater = True
Scale = 0.65 || Value = 0.26 || Changed = True || IOU = 0.24 || Greater = False
Scale = 0.64 || Value = 0.28 || Changed = True || IOU = 0.27 || Greater = True
Scale = 0.63 || Value = 0.24 || Changed = False || IOU = 0.24 || Greater = False
Scale = 0.62 || Value = 0.34 || Changed = True || IOU = 0.29 || Greater = True
Scale = 0.61 || Value = 0.32 || Changed = False || IOU = 0.29 || Greater = True
Scale = 0.60 || Value = 0.34 || Changed = True || IOU = 0.34 || Greater = True
Scale = 0.59 || Value = 0.29 || Changed = False || IOU = 0.30 || Greater = False
Scale = 0.58 || Value = 0.27 || Changed = False || IOU = 0.29 || Greater = False
Scale = 0.57 || Value = 0.32 || Changed = False || IOU = 0.29 || Greater = False
Scale = 0.56 || Value = 0.33 || Changed = False || IOU = 0.29 || Greater = False
Scale = 0.55 || Value = 0.33 || Changed = False || IOU = 0.29 || Greater = False
Scale = 0.54 || Value = 0.40 || Changed = True || IOU = 0.32 || Greater = False
Scale = 0.53 || Value = 0.27 || Changed = False || IOU = 0.25 || Greater = False
Scale = 0.52 || Value = 0.25 || Changed = False || IOU = 0.26 || Greater = False
================================================
Có vẻ như cách này khá hợp lý, cài đặt tính toán vào matching để show kết quả.
def search_best_scale(img, template_used, meth, scales, mask=None, show_progress=True):
"""
Duyệt qua các 'scales' để resize template và mask,
gọi lại hàm match_template để lấy kết quả,
trả về (best_loc, best_scale, best_min_val, best_max_val).
"""
h, w = template_used.shape[:2]
method = getattr(cv2, meth)
# Xác định giá trị khởi tạo tùy vào phương pháp
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
best_val = float('inf')
else:
best_val = float('-inf')
best_iou = -1.0
best_loc, best_scale = None, None
best_min_val, best_max_val = None, None
iterator = tqdm(scales, unit='scale', desc='\t') if show_progress else scales
# Duyệt qua từng scale để chọn kết quả hợp lý nhất
for scale in iterator:
new_h, new_w = int(h * scale), int(w * scale)
if new_h < 1 or new_w < 1:
continue # Bỏ qua nếu scale làm template về kích thước <= 0
template_resized = cv2.resize(template_used, (new_w, new_h))
if mask is not None:
mask_resized = cv2.resize(mask, (new_w, new_h))
else:
mask_resized = None
# Gọi hàm match_template đã viết
res_dict = match_template(img, template_resized, meth, mask=mask_resized)
cur_best_value = res_dict["best_value"]
x, y = res_dict["top_left"]
roi = img[y:y + new_h, x:x + new_w]
cur_iou = iou(template_resized, roi)
if cur_iou < 0:
continue
# Cập nhật kết quả tốt nhất
if cur_iou > best_iou:
# Cập nhật kết quả tốt nhất
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
if cur_best_value < best_val:
best_iou = cur_iou
best_val = cur_best_value
best_loc = res_dict["top_left"]
best_scale = scale
best_min_val = res_dict["min_val"]
best_max_val = res_dict["max_val"]
else:
if cur_best_value > best_val:
best_iou = cur_iou
best_val = cur_best_value
best_loc = res_dict["top_left"]
best_scale = scale
best_min_val = res_dict["min_val"]
best_max_val = res_dict["max_val"]
return best_loc, best_scale, best_min_val, best_max_val, best_iou, best_val
def template_matching_best_scale(templates, img, meth, scales, use_mask=True, show_progress=True):
"""
Thực hiện Template Matching với nhiều scale khác nhau trên danh sách template.
Args:
templates (list of np.ndarray): Danh sách các template cần tìm trên ảnh.
img (np.ndarray): Ảnh gốc để thực hiện template matching.
meth (str): Tên phương pháp template matching, ví dụ "TM_CCOEFF_NORMED".
scales (list of float): Danh sách tỉ lệ scale để resize (vd: [0.5, 1.0, 1.5]).
Returns:
list of dict: Mỗi phần tử chứa thông tin bounding box:
{
"top_left": (x, y),
"bottom_right": (x, y),
"min_val": float,
"max_val": float,
"best_value": float,
"method": str
}
"""
# Xác định kiểu ảnh gốc (color hoặc gray)
img_color_space = 'rgb' if len(img.shape) > 2 else 'gray'
results = []
for i, template in enumerate(templates):
if show_progress:
print(f'Template {i + 1}/{len(templates)}')
# Chuẩn bị template và mask
template_used, mask = prepare_template_and_mask(template, img_color_space)
if not use_mask:
mask = None
# Tìm best_loc, best_scale
best_loc, best_scale, best_min_val, best_max_val, best_iou, best_val = search_best_scale(
img, template_used, meth, scales, mask, show_progress
)
# Tính bounding box
h, w = template_used.shape[:2]
top_left, bottom_right = compute_bounding_box(best_loc, best_scale, w, h)
# Lưu dict kết quả
results.append({
"top_left": top_left,
"bottom_right": bottom_right,
"min_val": best_min_val,
"max_val": best_max_val,
"best_iou": best_iou,
"best_val": best_val,
"method": meth
})
return results
scales = np.arange(1.0, 0.51, -0.01)
print(len(scales))
methods = [
"TM_CCOEFF_NORMED"
]
for image, edged_image, template_set in zip(images, edged_images, edged_templates):
img_res = image.copy()
for i, method in enumerate(methods):
match_result = template_matching_best_scale(templates=template_set, meth=method, img=edged_image, scales=scales, show_progress=False, use_mask=False)
img_res = draw_bounding_box(img_res, match_result)
show(template_set, figsize=(3, 3))
show(img_res, figsize=(5, 9))
49
4. Kết luận¶
Cuối cùng, chúng ta đúc kết được quy trình thực hiện như sau:
- Load ảnh và template và các list()
- Xác định cạnh trong cách hình bằng Canny
- Tiến hành matching từng template theo từng scale, đồng thời lưu lại giá trị IOU và matching tốt nhất để so sánh, nếu đồng thời vượt qua 2 giá trị đó thì chọn làm kết quả.
- Show kết quả
Dưới đây là tổng hợp code.
import cv2
import matplotlib.pyplot as plt
import numpy as np
import os
import glob
import math
from tqdm import tqdm
def show_image(image, figsize=None):
if figsize is not None:
plt.figure(figsize=figsize)
if len(image.shape) > 2:
image = image[...,::-1]
plt.imshow(image)
plt.axis('off')
def show(to_show, figsize=(10, 10), plot_size=None):
"""
Hiển thị một hoặc nhiều ảnh bằng Matplotlib.
Nếu chỉ có một ảnh, hàm sẽ hiển thị ảnh duy nhất đó.
Nếu có nhiều ảnh, hàm sẽ hiển thị theo lưới (grid) con dựa trên `plot_size` (nếu được cung cấp).
Args:
images (list of np.ndarray): Danh sách ảnh để hiển thị.
figsize (tuple of int, optional): Kích thước của figure. Mặc định là (10, 10).
plot_size (tuple of int, optional): Kích thước lưới hiển thị, dạng (rows, cols).
Nếu không được truyền vào, ảnh sẽ được hiển thị tuần tự trên các subplot liên tiếp.
Returns:
None: Hàm không trả về giá trị, chỉ hiển thị ảnh bằng matplotlib.
"""
# If you want to show only one images[0].
if not isinstance(to_show, list):
plt.figure(figsize=figsize)
show_image(to_show)
return
# Else
n = len(to_show)
if plot_size is None:
cols = int(math.ceil(math.sqrt(n)))
rows = int(math.ceil(n / cols))
else:
rows, cols = plot_size
# Hiển thị tuần tự
plt.figure(figsize=figsize)
for i, image in enumerate(to_show):
plt.subplot(rows, cols, i + 1)
show_image(image)
# Ẩn các subplot dư (nếu hàng*cột > n)
for idx in range(n, rows * cols):
plt.subplot(rows, cols, idx + 1)
plt.axis('off')
plt.tight_layout()
plt.show()
def get_all_templates(dir, name_tag='t'):
"""
Đọc tất cả file ảnh trong thư mục `dir` có tên bắt đầu với `name_tag`
và phần mở rộng .png. Trả về danh sách các ảnh (templates).
Args:
dir (str): Đường dẫn đến thư mục chứa ảnh.
name_tag (str, optional): Tiền tố dùng để lọc tên file. Mặc định là 't'.
Returns:
list[np.ndarray]: Danh sách các ảnh (templates) được đọc (dưới dạng numpy array).
"""
pattern = os.path.join(dir, f"{name_tag}*.png")
file_list = sorted(glob.glob(pattern))
templates = []
for path in file_list:
template = cv2.imread(path)
templates.append(template)
return templates
def convert_color(images, color='rgb'):
"""
Chuyển đổi không gian màu cho một ảnh hoặc danh sách ảnh sang không gian màu chỉ định.
Args:
images (numpy.ndarray | list): Ảnh đầu vào hoặc danh sách ảnh.
color (str, optional): Kiểu màu muốn chuyển, như 'rgb' hoặc 'gray'. Mặc định là 'rgb'.
Returns:
numpy.ndarray | list: Ảnh (hoặc danh sách ảnh) sau khi chuyển đổi màu.
"""
if not isinstance(images, list):
return cv2.cvtColor(images, color)
res = []
for image in images:
res.append(cv2.cvtColor(image, color))
return res
def match_template(image, template, meth, mask=None):
"""
Thực hiện template matching với một phương pháp duy nhất trên một cặp ảnh/mặt nạ.
Trả về dict chứa vị trí top_left/bottom_right và các giá trị min_val, max_val.
"""
# Lấy constant của OpenCV từ meth (VD: CV_TM_CCOEFF, ...)
method = getattr(cv2, meth)
# Truyền thêm mask nếu cần
if mask is not None and method in [cv2.TM_SQDIFF, cv2.TM_CCORR]:
res = cv2.matchTemplate(image, template, method, mask=mask)
else:
res = cv2.matchTemplate(image, template, method)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
# Tùy vào phương pháp, top_left sẽ là min_loc hoặc max_loc
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
top_left = min_loc
best_value = min_val
else:
top_left = max_loc
best_value = max_val
h, w = template.shape[:2]
bottom_right = (top_left[0] + w, top_left[1] + h)
return {
"top_left": top_left,
"bottom_right": bottom_right,
"min_val": min_val,
"max_val": max_val,
"best_value": best_value,
"method": meth
}
def draw_bounding_box(image, match_result, color=(0, 0, 0), in_place=False, thickness=4):
"""
Vẽ bounding box trực tiếp lên ảnh dựa trên kết quả match_result
rồi trả về ảnh kết quả. Không sử dụng matplotlib để hiển thị.
Args:
image (np.ndarray): Ảnh gốc (BGR hoặc grayscale).
match_result (dict | list[dict]):
- dict: dạng {
"top_left": (x, y),
"bottom_right": (x, y),
"min_val": float,
"max_val": float,
"best_value": float,
"method": str (tuỳ chọn)
}
- list[dict]: danh sách các dict giống trên.
Returns:
np.ndarray: Ảnh đã được vẽ bounding box (cùng kích thước với ảnh gốc).
"""
import cv2
# Nếu match_result là dict, gói thành danh sách để nạp vòng lặp
if isinstance(match_result, dict):
match_result = [match_result]
if in_place:
image_to_draw = image
else:
image_to_draw = image.copy()
# Vẽ bounding box cho từng dict
for res in match_result:
top_left = res["top_left"]
bottom_right = res["bottom_right"]
cv2.rectangle(image_to_draw, top_left, bottom_right, color, thickness)
return image_to_draw
def prepare_template_and_mask(template, img_color_space):
"""
Nếu ảnh gốc là grayscale thì chuyển template sang gray.
Ngược lại vẫn dùng template gốc, đồng thời tạo mask bằng ngưỡng trên ảnh gray.
"""
# Nếu ảnh gốc là gray
if img_color_space == 'gray':
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) if len(template.shape) > 2 else template
template_used = template_gray
else:
# Ảnh gốc là color
template_used = template
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) if len(template.shape) > 2 else template
# Tạo mask foreground bằng threshold
_, mask = cv2.threshold(template_gray, 5, 255, cv2.THRESH_BINARY)
return template_used, mask
def compute_bounding_box(best_loc, best_scale, w, h):
"""
Tính toạ độ góc trên - trái và dưới - phải cho bounding box dựa trên best_scale.
"""
top_left = best_loc
h_resize = int(h * best_scale)
w_resize = int(w * best_scale)
bottom_right = (top_left[0] + w_resize, top_left[1] + h_resize)
return top_left, bottom_right
def to_edges(img, blur_ksize=3, threshold1=50, threshold2=200):
"""
Tiền xử lý ảnh bằng Canny:
1) Chuyển sang gray nếu cần
2) Làm mờ ảnh (GaussianBlur)
3) Lấy biên cạnh (Canny)
4) Áp dụng phép đóng (morphological close) để nối các cạnh đứt đoạn
"""
# Nếu là ảnh BGR, chuyển về gray
if len(img.shape) == 3:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
gray = img
# Giảm nhiễu
# blurred = cv2.GaussianBlur(gray, (blur_ksize, blur_ksize), 0)
# Morphological close để lấp kín các lỗ hổng nhỏ
# kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# blurred = cv2.morphologyEx(blurred, cv2.MORPH_OPEN, kernel)
# Lấy biên cạnh
edges = cv2.Canny(gray, threshold1, threshold2)
return edges
def iou(image1, image2, show_=False):
object_mask = image1 != 0
object_in_background_mask = image2 != 0
intersection = object_mask & object_in_background_mask
if show_:
show(intersection, figsize=(1, 1))
overlap_sum = np.sum(intersection)
union = np.sum(object_mask | object_in_background_mask) # Cả hai ảnh có kích thước giống nhau
similarity_score = overlap_sum / union if union != 0 else 0
return similarity_score
def search_best_scale(img, template_used, meth, scales, mask=None, show_progress=True):
"""
Duyệt qua các 'scales' để resize template và mask,
gọi lại hàm match_template để lấy kết quả,
trả về (best_loc, best_scale, best_min_val, best_max_val).
"""
h, w = template_used.shape[:2]
method = getattr(cv2, meth)
# Xác định giá trị khởi tạo tùy vào phương pháp
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
best_val = float('inf')
else:
best_val = float('-inf')
best_iou = -1.0
best_loc, best_scale = None, None
best_min_val, best_max_val = None, None
iterator = tqdm(scales, unit='scale', desc='\t') if show_progress else scales
# Duyệt qua từng scale để chọn kết quả hợp lý nhất
for scale in iterator:
new_h, new_w = int(h * scale), int(w * scale)
if new_h < 1 or new_w < 1:
continue # Bỏ qua nếu scale làm template về kích thước <= 0
template_resized = cv2.resize(template_used, (new_w, new_h))
if mask is not None:
mask_resized = cv2.resize(mask, (new_w, new_h))
else:
mask_resized = None
# Gọi hàm match_template đã viết
res_dict = match_template(img, template_resized, meth, mask=mask_resized)
cur_best_value = res_dict["best_value"]
x, y = res_dict["top_left"]
roi = img[y:y + new_h, x:x + new_w]
cur_iou = iou(template_resized, roi)
if cur_iou < 0:
continue
# Cập nhật kết quả tốt nhất
if cur_iou > best_iou:
# Cập nhật kết quả tốt nhất
if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
if cur_best_value < best_val:
best_iou = cur_iou
best_val = cur_best_value
best_loc = res_dict["top_left"]
best_scale = scale
best_min_val = res_dict["min_val"]
best_max_val = res_dict["max_val"]
else:
if cur_best_value > best_val:
best_iou = cur_iou
best_val = cur_best_value
best_loc = res_dict["top_left"]
best_scale = scale
best_min_val = res_dict["min_val"]
best_max_val = res_dict["max_val"]
return best_loc, best_scale, best_min_val, best_max_val, best_iou, best_val
def template_matching_best_scale(templates, img, meth, scales, use_mask=True, show_progress=True):
"""
Thực hiện Template Matching với nhiều scale khác nhau trên danh sách template.
Args:
templates (list of np.ndarray): Danh sách các template cần tìm trên ảnh.
img (np.ndarray): Ảnh gốc để thực hiện template matching.
meth (str): Tên phương pháp template matching, ví dụ "TM_CCOEFF_NORMED".
scales (list of float): Danh sách tỉ lệ scale để resize (vd: [0.5, 1.0, 1.5]).
Returns:
list of dict: Mỗi phần tử chứa thông tin bounding box:
{
"top_left": (x, y),
"bottom_right": (x, y),
"min_val": float,
"max_val": float,
"best_value": float,
"method": str
}
"""
# Xác định kiểu ảnh gốc (color hoặc gray)
img_color_space = 'rgb' if len(img.shape) > 2 else 'gray'
results = []
for i, template in enumerate(templates):
if show_progress:
print(f'Template {i + 1}/{len(templates)}')
# Chuẩn bị template và mask
template_used, mask = prepare_template_and_mask(template, img_color_space)
if not use_mask:
mask = None
# Tìm best_loc, best_scale
best_loc, best_scale, best_min_val, best_max_val, best_iou, best_val = search_best_scale(
img, template_used, meth, scales, mask, show_progress
)
# Tính bounding box
h, w = template_used.shape[:2]
top_left, bottom_right = compute_bounding_box(best_loc, best_scale, w, h)
# Lưu dict kết quả
results.append({
"top_left": top_left,
"bottom_right": bottom_right,
"min_val": best_min_val,
"max_val": best_max_val,
"best_iou": best_iou,
"best_val": best_val,
"method": meth
})
return results
images = [cv2.imread(f'Finding/{i + 1}/image.png') for i in range(2)]
templates = [get_all_templates(f'Finding/{i + 1}') for i in range(2)]
print('Finding set 1:')
show(images[0], figsize=(5, 8))
show(templates[0], figsize=(3, 3))
print('Finding set 2:')
show(images[1], figsize=(5, 8))
show(templates[1], figsize=(3, 3))
Finding set 1:
Finding set 2:
edged_images = [to_edges(img) for img in images]
edged_templates = []
for tmpl_list in templates:
edgelist = [to_edges(tmpl) for tmpl in tmpl_list]
edged_templates.append(edgelist)
for image, tempset in zip(edged_images, edged_templates):
show(tempset, figsize=(3, 3))
show(image, figsize=(5, 8))
scales = np.arange(1.0, 0.51, -0.01)
print(len(scales))
methods = [
"TM_CCOEFF_NORMED"
]
for image, edged_image, template_set in zip(images, edged_images, edged_templates):
img_res = image.copy()
for i, method in enumerate(methods):
match_result = template_matching_best_scale(templates=template_set, meth=method, img=edged_image, scales=scales, show_progress=False, use_mask=False)
img_res = draw_bounding_box(img_res, match_result)
show(template_set, figsize=(3, 3))
show(img_res, figsize=(5, 9))
49